﻿namespace SuperModel.Generation
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Reflection.Emit;

    /// <summary>
    /// The type implementer
    /// </summary>
    internal class TypeImplementer
    {
        /// <summary>
        /// The type namer
        /// </summary>
        private readonly TypeNamer namer = new TypeNamer();

        /// <summary>
        /// The interface implementer
        /// </summary>
        private readonly InterfaceImplementer interfaceImplementer = new InterfaceImplementer();

        /// <summary>
        /// The attribute transmitter.
        /// </summary>
        private readonly AttributeTransmitter attributeTransmitter = new AttributeTransmitter();

        /// <summary>
        /// Implements a Type
        /// </summary>
        /// <param name="typeToImplement">
        /// The type to implement.
        /// </param>
        /// <param name="moduleBuilder">
        /// The module builder.
        /// </param>
        public void ImplementType(Type typeToImplement, ModuleBuilder moduleBuilder)
        {
            // Get The Type Builder
            var baseType = TypeToImplementImplementsSuperModel(typeToImplement) ? typeof(BaseSuperModel) : typeof(BaseNotifyPropertyChanged);
            TypeBuilder newType = moduleBuilder.DefineType(namer.GetImplementationName(typeToImplement), TypeAttributes.Public | TypeAttributes.Class, baseType);

            attributeTransmitter.ApplyAttributesToType(typeToImplement, newType);

            // Keep track of all the fields added to the model
            var fields = new List<FieldBuilder>();

            // Get the Types to Implement
            var interfacesToImplement = new List<Type>(typeToImplement.GetInterfaces())
                                                    {
                                                        typeToImplement
                                                    };

            var dontImplementInterfaces = new List<Type> { typeof(INotifyPropertyChanged) };

            // Implement the Interfaces
            foreach (Type interfaceToImplement in interfacesToImplement.Except(dontImplementInterfaces))
            {
                // Ignore any Super-Model Specifications
                if (interfaceToImplement.IsGenericType)
                {
                    if (interfaceToImplement == typeof(INotifyPropertyChanged))
                    {
                        continue;
                    }
                }

                interfaceImplementer.ImplementInterface(interfaceToImplement, newType, fields);
            }

            // Implement the Constructor
            ImplementConstructor(newType);

            // Create the Type
            newType.CreateType();
        }

        /// <summary>
        /// Types to implement implements super model.
        /// </summary>
        /// <param name="typeToImplement">The type to implement.</param>
        /// <returns>A value indicating whether the type to implement implements ISuperModel</returns>
        private static bool TypeToImplementImplementsSuperModel(Type typeToImplement)
        {
            foreach (var implementedInterface in typeToImplement.GetInterfaces())
            {
                if (typeof(ISuperModel).IsAssignableFrom(implementedInterface))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Implements the Constructor
        /// </summary>
        /// <param name="newType">
        /// The new Type.
        /// </param>
        private static void ImplementConstructor(TypeBuilder newType)
        {
            const MethodAttributes Attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName;

            // Define the Constructor
            ConstructorBuilder ctor = newType.DefineConstructor(Attributes, CallingConventions.HasThis, Type.EmptyTypes);

            // Generate the Code
            var gen = ctor.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
            gen.Emit(OpCodes.Nop);
            gen.Emit(OpCodes.Ret);
        }
    }
}
